iT邦幫忙

2024 iThome 鐵人賽

DAY 22
2
Modern Web

Vue 和 TypeScript 的最佳實踐:成為前端工程師的進階利器系列 第 22

Day 22: 使用 TypeScript 和 Vitest 測試 Vue 組件的邊界情況

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20240925/20117461K4wFLB4Iuf.jpg

簡介

在開發 Vue 應用時,測試邊界情況對於確保組件的穩定性和可靠性至關重要。本文將探討如何使用 TypeScript 和 Vitest 來測試 Vue 組件的各種邊界情況。我們將基於Day20先前開發的組件,如卡片、輸入框、等,展示如何編寫全面的測試用例。同時,展示如何在複雜的場景中進行測試。
建議讀者對測試有興趣的,可以嘗試自己把元件做出來後自己進行測試驗證。

步驟 1: 測試卡片組件 (Card)

讓我們從測試卡片組件開始。我們將測試不同的變體和邊界情況。

(檔案: src/components/Card.vue)

<script setup lang="ts">
  const { testID = 'card' } = defineProps<{
    testID?: string;
  }>();
</script>

<template>
  <div :data-testid="testID" px-6 py-4 rounded-md shadow-xl bg-gray-100>
    <slot />
  </div>
</template>

(檔案:src/components/Card.spec.ts)

import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import Card from './Card.vue';


describe('Card.vue', () => {
  it('render default props', () => {
    const wrapper = mount(Card);
    expect(wrapper.attributes('data-testid')).toBe('card')
  });

  it('render props', () => {
    const testID = 'hello';
    const wrapper = mount(Card, {
      props: {
        testID,
      }
    });
    expect(wrapper.attributes('data-testid')).toBe(testID)
  });

  it('render slot', () => {
    const defaultSlotTemplate = 'this is cool';
    const wrapper = mount(Card, {
      slots: {
        default: defaultSlotTemplate,
      },
    });
    expect(wrapper.text()).toContain(defaultSlotTemplate);
  });

  it('UnoCss attribute', () => {
    const wrapper = mount(Card);
    expect(wrapper.attributes('px-6')).toBeDefined();
    expect(wrapper.attributes('py-4')).toBeDefined();
    expect(wrapper.attributes('rounded-md')).toBeDefined();
    expect(wrapper.attributes('shadow-xl')).toBeDefined();
  });
});

步驟 2: 測試輸入框組件 (Input)

接下來,我們將測試輸入框組件,包括驗證和錯誤處理的邊界情況。

(檔案 : src/components/input/TextInputSmall.vue)

<script setup lang="ts">
  import { useId } from 'vue';

  const { id = useId(), isShowLabel = true, errorMessage = '', disabled = false } = defineProps<{
    label: string;
    id?: string;
    isShowLabel?: boolean;
    errorMessage?: string;
    disabled?: boolean;
  }>();

  const errorID = useId();
  const modelValue = defineModel<string | number>({ default: '' });
</script>

<template>
  <div>
    <label v-show="isShowLabel" :for="id">{{ label }}</label>
    <input 
      w-full
      px-2 py-1 
      rounded-md 
      outline-none
      shadow-lg 
      :aria-describedby="errorMessage ? errorID : undefined" 
      :id :disabled 
      v-model="modelValue" 
    />
    <span v-show="errorMessage" :id="errorID">{{ errorMessage }}</span>
  </div>
</template>

(檔案: src/components/input/TextInputSmall.spec.ts)

import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils'
import TextInputSmall from './TextInputSmall.vue';

describe('TextInputSmall.vue', () => {
  it('true', () => {
    expect(true).toBe(true);
  });
});


describe('TextInputSmall', () => {
  it('renders the label when isShowLabel is true', () => {
    const wrapper = mount(TextInputSmall, {
      props: {
        label: 'Test Label',
        isShowLabel: true
      }
    })
    expect(wrapper.find('label').exists()).toBe(true)
    expect(wrapper.find('label').text()).toBe('Test Label')
  })

  it('does not render the label when isShowLabel is false', () => {
    const wrapper = mount(TextInputSmall, {
      props: {
        label: 'Test Label',
        isShowLabel: false
      }
    })
    const label = wrapper.find('label');
    expect(label.attributes('style')).toContain('display: none;');
  })

  it('binds the id prop to both label and input', () => {
    const wrapper = mount(TextInputSmall, {
      props: {
        label: 'Test Label',
        id: 'test-id'
      }
    })
    expect(wrapper.find('label').attributes('for')).toBe('test-id')
    expect(wrapper.find('input').attributes('id')).toBe('test-id')
  })

  it('shows error message when provided', async () => {
    const wrapper = mount(TextInputSmall, {
      props: {
        label: 'Test Label',
        errorMessage: 'Error occurred'
      }
    })
    expect(wrapper.find('span').exists()).toBe(true)
    expect(wrapper.find('span').text()).toBe('Error occurred')
  
    const inputElement = wrapper.find('input')
    const errorSpan = wrapper.find('span')
    expect(inputElement.attributes('aria-describedby')).toBe(errorSpan.attributes('id'))
  })

  it('does not show error message when not provided', () => {
    const wrapper = mount(TextInputSmall, {
      props: {
        label: 'Test Label'
      }
    })

    const errorMessageSpan = wrapper.find('span');
    expect(errorMessageSpan.attributes('style')).toContain('display: none;');
    expect(errorMessageSpan.text()).toBe('');
  })

  it('disables the input when disabled prop is true', () => {
    const wrapper = mount(TextInputSmall, {
      props: {
        label: 'Test Label',
        disabled: true
      }
    })
    expect(wrapper.find('input').element.disabled).toBe(true)
  })

  it('updates the modelValue when input changes', async () => {
    const wrapper = mount(TextInputSmall, {
      props: {
        label: 'Test Label',
        modelValue: ''
      }
    })
    await wrapper.find('input').setValue('New Value')
    expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['New Value'])
  })

  it('applies the correct CSS classes to the input', () => {
    const wrapper = mount(TextInputSmall, {
      props: {
        label: 'Test Label'
      }
    })
    const inputClasses = wrapper.find('input');
    expect(inputClasses.attributes('w-full')).toBeDefined();
    expect(inputClasses.attributes('px-2')).toBeDefined();
    expect(inputClasses.attributes('py-1')).toBeDefined();
    expect(inputClasses.attributes('rounded-md')).toBeDefined();
    expect(inputClasses.attributes('outline-none')).toBeDefined();
    expect(inputClasses.attributes('shadow-lg')).toBeDefined();
  })
})

結論

在本文中,我們深入探討了如何使用 TypeScript 和 Vitest 來測試 Vue 組件的邊界情況。我們針對卡片、輸入框等組件進行了全面的測試,涵蓋了各種可能的使用場景和邊界情況。

通過這些測試,我們不僅確保了組件在正常情況下的功能正確性,還驗證了它們在非預期輸入、異常狀態和極端情況下的行為。這種全面的測試方法有助於提高組件的穩定性和可靠性,同時也為未來的重構和功能擴展提供了保障。

在實際開發中,你可能需要根據具體的業務邏輯和用戶需求來調整和擴展這些測試。記住,好的測試不僅能捕捉錯誤,還能幫助你更好地理解和改進你的代碼。持續地編寫和維護測試,將有助於你構建更加健壯和可維護的 Vue 應用。


上一篇
Day 21: Vitest 和 @vue/test-utils 的基礎介紹:如何編寫單元測試
下一篇
Day 23: 如何測試 Vue Router 的導航邏輯與 Pinia 的狀態管理
系列文
Vue 和 TypeScript 的最佳實踐:成為前端工程師的進階利器30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言